package ctrlsubsonic

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"image"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/disintegration/imaging"
	"github.com/jinzhu/gorm"

	"go.senan.xyz/gonic/db"
	"go.senan.xyz/gonic/infocache/artistinfocache"
	"go.senan.xyz/gonic/server/ctrlsubsonic/params"
	"go.senan.xyz/gonic/server/ctrlsubsonic/spec"
	"go.senan.xyz/gonic/server/ctrlsubsonic/specid"
	"go.senan.xyz/gonic/server/ctrlsubsonic/specidpaths"
	"go.senan.xyz/gonic/tags"
	"go.senan.xyz/gonic/transcode"
)

// "raw" handlers are ones that don't always return a spec response.
// it could be a file, stream, etc. so you must either
//   a) write to response writer
//   b) return a non-nil spec.Response
//  _but not both_

const (
	coverDefaultSize = 600
)

var (
	//nolint:gochecknoglobals
	// after the image is decoded with image.Decode, we use the "format" param as the extension
	coverCacheFormats = []string{
		"jpeg", // from image.RegisterFormat("jpeg") @ image/jpeg/reader.go
		"png",  // from image.RegisterFormat("png")  @ image/png/reader.go
		"gif",  // from image.RegisterFormat("gif")  @ image/gif/reader.go
		"bmp",  // from image.RegisterFormat("bmp")  @ golang.org/x/image/bmp/reader.go
		"tiff", // from image.RegisterFormat("tiff") @ golang.org/x/image/tiff/reader.go
	}
)

func (c *Controller) ServeGetCoverArt(w http.ResponseWriter, r *http.Request) *spec.Response {
	params := r.Context().Value(CtxParams).(params.Params)
	id, err := params.GetID("id")
	if err != nil {
		return spec.NewError(10, "please provide an `id` parameter")
	}

	size := params.GetOrInt("size", coverDefaultSize)
	if size <= 0 {
		return spec.NewError(0, "invalid size")
	}

	// cached cover could exist for any supported format
	cachePath, err := findCachedCover(c.cacheCoverPath, id.String(), size)
	if err != nil && !os.IsNotExist(err) {
		log.Printf("error checking cache: %v", err)
		return nil
	}

	serve := func(p string) {
		w.Header().Set("Cache-Control", "public, max-age=1209600")
		http.ServeFile(w, r, p)
	}

	if cachePath != "" {
		serve(cachePath)
		return nil
	}

	reader, err := coverFor(c.dbc, c.artistInfoCache, c.tagReader, id)
	if err != nil {
		return spec.NewError(10, "couldn't find cover %q: %v", id, err)
	}
	defer reader.Close()

	img, format, err := image.Decode(reader)
	if err != nil {
		return spec.NewError(0, "decode for cover %q: %v", id, err)
	}

	// don't upscale
	minSize := min(size, max(img.Bounds().Dx(), img.Bounds().Dy()))

	cachePath = filepath.Join(c.cacheCoverPath, coverCacheFilename(id.String(), minSize, format))

	if minSize != size {
		// we down sized, check cache again
		if _, err := os.Stat(cachePath); err == nil {
			serve(cachePath)
			return nil
		}
	}

	resized := imaging.Fit(img, minSize, minSize, imaging.Lanczos)

	if err := imaging.Save(resized, cachePath); err != nil {
		return spec.NewError(10, "saving cover %q: %v", id, err)
	}

	serve(cachePath)
	return nil
}

func coverCacheFilename(idStr string, size int, format string) string {
	return fmt.Sprintf("%s-%d.%s", idStr, size, format)
}

func findCachedCover(cacheDir, idStr string, size int) (string, error) {
	for _, format := range coverCacheFormats {
		cachePath := filepath.Join(cacheDir, coverCacheFilename(idStr, size, format))
		if _, err := os.Stat(cachePath); err == nil {
			return cachePath, nil
		}
	}
	return "", os.ErrNotExist
}

var (
	errCoverNotFound = errors.New("could not find a cover with that id")
	errCoverEmpty    = errors.New("no cover found")
)

// TODO: can we use specidpaths.Locate here?
func coverFor(dbc *db.DB, artistInfoCache *artistinfocache.ArtistInfoCache, tagReader tags.Reader, id specid.ID) (io.ReadCloser, error) {
	switch id.Type {
	case specid.Album:
		return coverForAlbum(dbc, id.Value)
	case specid.Artist:
		return coverForArtist(artistInfoCache, id.Value)
	case specid.Podcast:
		return coverForPodcast(dbc, id.Value)
	case specid.PodcastEpisode:
		return coverForPodcastEpisode(dbc, id.Value)
	case specid.Track:
		return coverForTrack(dbc, tagReader, id.Value)
	default:
		return nil, errCoverNotFound
	}
}

func coverForAlbum(dbc *db.DB, id int) (*os.File, error) {
	var folder db.Album
	err := dbc.DB.
		Select("id, root_dir, left_path, right_path, cover").
		First(&folder, id).
		Error
	if err != nil {
		return nil, fmt.Errorf("select album: %w", err)
	}
	if folder.Cover == "" {
		return nil, errCoverEmpty
	}
	return os.Open(filepath.Join(folder.RootDir, folder.LeftPath, folder.RightPath, folder.Cover))
}

func coverForArtist(artistInfoCache *artistinfocache.ArtistInfoCache, id int) (io.ReadCloser, error) {
	info, err := artistInfoCache.Get(context.Background(), id)
	if err != nil {
		return nil, fmt.Errorf("get artist info from cache: %w", err)
	}
	if info.ImageURL == "" {
		return nil, fmt.Errorf("%w: cache miss", errCoverEmpty)
	}
	resp, err := http.Get(info.ImageURL)
	if err != nil {
		return nil, fmt.Errorf("req image from lastfm: %w", err)
	}
	return resp.Body, nil
}

func coverForPodcast(dbc *db.DB, id int) (*os.File, error) {
	var podcast db.Podcast
	err := dbc.
		First(&podcast, id).
		Error
	if err != nil {
		return nil, fmt.Errorf("select podcast: %w", err)
	}
	if podcast.Image == "" {
		return nil, errCoverEmpty
	}
	return os.Open(filepath.Join(podcast.RootDir, podcast.Image))
}

func coverForPodcastEpisode(dbc *db.DB, id int) (*os.File, error) {
	var pe db.PodcastEpisode
	err := dbc.
		Preload("Podcast").
		First(&pe, id).
		Error
	if err != nil {
		return nil, fmt.Errorf("select episode: %w", err)
	}
	if pe.Podcast == nil || pe.Podcast.Image == "" {
		return nil, errCoverEmpty
	}
	return os.Open(filepath.Join(pe.Podcast.RootDir, pe.Podcast.Image))
}

func coverForTrack(dbc *db.DB, tagReader tags.Reader, id int) (io.ReadCloser, error) {
	var tr db.Track
	err := dbc.
		Preload("Album").
		First(&tr, id).
		Error
	if err != nil {
		return nil, fmt.Errorf("select track: %w", err)
	}

	absPath := tr.AbsPath()

	cover, err := tagReader.ReadCover(absPath)
	if err != nil {
		return nil, fmt.Errorf("read cover: %w", err)
	}
	if len(cover) == 0 {
		return nil, errCoverEmpty
	}

	return io.NopCloser(bytes.NewReader(cover)), nil
}

func (c *Controller) ServeStream(w http.ResponseWriter, r *http.Request) *spec.Response {
	params := r.Context().Value(CtxParams).(params.Params)
	user := r.Context().Value(CtxUser).(*db.User)
	id, err := params.GetID("id")
	if err != nil {
		return spec.NewError(10, "please provide an `id` parameter")
	}

	file, err := specidpaths.Locate(c.dbc, id)
	if err != nil {
		return spec.NewError(0, "error looking up id %s: %v", id, err)
	}

	audioFile, ok := file.(db.AudioFile)
	if !ok {
		return spec.NewError(0, "type of id does not contain audio")
	}

	maxBitRate, _ := params.GetInt("maxBitRate")
	format, _ := params.Get("format")
	timeOffset, _ := params.GetInt("timeOffset")

	if format == "raw" {
		http.ServeFile(w, r, file.AbsPath())
		return nil
	}

	client, _ := params.Get("c")
	pref, err := streamGetTranscodePreference(c.dbc, user.ID, client)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return spec.NewError(0, "couldn't find transcode preference: %v", err)
	}
	if pref == nil {
		if maxBitRate > 0 && maxBitRate < audioFile.AudioBitrate() {
			return spec.NewError(0, "param maxBitRate requested and no user transcode preferences found for user %q and client %q. please configure transcode settings if you want to transcode", user.Name, client)
		}
		log.Printf("serving raw file, no user transcode preferences found for user %q and client %q", user.Name, client)
		http.ServeFile(w, r, file.AbsPath())
		return nil
	}

	if maxBitRate >= audioFile.AudioBitrate() {
		log.Printf("serving raw file, requested max bitrate %d is greater or equal to %d", maxBitRate, audioFile.AudioBitrate())
		http.ServeFile(w, r, file.AbsPath())
		return nil
	}

	profile, ok := transcode.UserProfiles[pref.Profile]
	if !ok {
		return spec.NewError(0, "unknown transcode user profile %q", pref.Profile)
	}
	if maxBitRate > 0 && int(profile.BitRate()) > maxBitRate {
		profile = transcode.WithBitrate(profile, transcode.BitRate(maxBitRate))
	}
	if timeOffset > 0 {
		profile = transcode.WithSeek(profile, time.Second*time.Duration(timeOffset))
	}

	log.Printf("transcoding to %q with at bitrate %d", profile.MIME(), profile.BitRate())

	w.Header().Set("Content-Type", profile.MIME())
	if err := c.transcoder.Transcode(r.Context(), profile, file.AbsPath(), w); err != nil && !errors.Is(err, transcode.ErrFFmpegKilled) {
		return spec.NewError(0, "error transcoding: %v", err)
	}

	if f, ok := w.(http.Flusher); ok {
		f.Flush()
	}
	return nil
}

func (c *Controller) ServeGetAvatar(w http.ResponseWriter, r *http.Request) *spec.Response {
	params := r.Context().Value(CtxParams).(params.Params)
	user := r.Context().Value(CtxUser).(*db.User)
	username, err := params.Get("username")
	if err != nil {
		return spec.NewError(10, "please provide an `username` parameter")
	}
	reqUser := c.dbc.GetUserByName(username)
	if (user.ID != reqUser.ID) && !user.IsAdmin {
		return spec.NewError(50, "user not admin")
	}
	http.ServeContent(w, r, "", time.Now(), bytes.NewReader(reqUser.Avatar))
	return nil
}

func streamGetTranscodePreference(dbc *db.DB, userID int, client string) (*db.TranscodePreference, error) {
	var pref db.TranscodePreference
	err := dbc.
		Where("user_id=?", userID).
		Where("client COLLATE NOCASE IN (?)", []string{"*", client}).
		Order("client DESC"). // ensure "*" is last if it's there
		First(&pref).
		Error
	if errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, fmt.Errorf("find transcode preference: %w", err)
	}
	return &pref, nil
}

func streamGetTranscodeMeta(dbc *db.DB, userID int, client string) spec.TranscodeMeta {
	pref, _ := streamGetTranscodePreference(dbc, userID, client)
	if pref == nil {
		return spec.TranscodeMeta{}
	}
	profile, ok := transcode.UserProfiles[pref.Profile]
	if !ok {
		return spec.TranscodeMeta{}
	}
	return spec.TranscodeMeta{
		TranscodedContentType: profile.MIME(),
		TranscodedSuffix:      profile.Suffix(),
	}
}
